using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Perovich.GameObjects.Interfaces;
using Perovich.GameObjects.Controls;
using System.Collections.ObjectModel;

namespace Perovich.GameObjects.TwoDimensional.Particles
{
    /// <summary>
    /// ParticleSystem is an abstract class that provides the basic functionality to
    /// create a particle effect. Different subclasses will have different effects,
    /// such as fire, explosions, and plumes of smoke. To use these subclasses, 
    /// simply call AddParticles, and pass in where the particles should exist.
    /// </summary>
    public abstract class ParticleSystem : Screen
    {
        /// <summary>
        /// particles are the little bits that will make up an effect. each effect will
        /// be comprised of many of these particles. They have basic physical properties,
        /// such as position, velocity, acceleration, and rotation. They'll be drawn as
        /// sprites, all layered on top of one another, and will be very pretty.
        /// </summary>
        protected internal struct Particle
        {
            // Position, Velocity, and Acceleration represent exactly what their names
            // indicate. They are public fields rather than properties so that users
            // can directly access their .X and .Y properties.
            private Vector2 position;


            /// <summary>
            /// The position.
            /// </summary>
            public Vector2 Position
            {
                get { return position; }
            }
            private Vector2 velocity;
            
            /// <summary>
            /// The Velocity
            /// </summary>
            public Vector2 Velocity
            {
                get { return velocity; }
            }
            private Vector2 acceleration;

            /// <summary>
            /// The acceleration.
            /// </summary>
            public Vector2 Acceleration
            {
                get { return acceleration; }
            }

            // how long this particle will "live"
            private float lifetime;
            /// <summary>
            /// The lifetime.
            /// </summary>
            public float Lifetime
            {
                get { return lifetime; }
            }

            // how long it has been since initialize was called
            private float timeSinceStart;
            /// <summary>
            /// Time since initialization.
            /// </summary>
            public float TimeSinceStart
            {
                get { return timeSinceStart; }
            }

            // the scale of this particle
            private float scale;
            /// <summary>
            /// The scale.
            /// </summary>
            public float Scale
            {
                get { return scale; }
            }

            // its rotation, in radians
            private float rotation;
            /// <summary>
            /// The rotation.
            /// </summary>
            public float Rotation
            {
                get { return rotation; }
            }

            // how fast does it rotate?
            private float rotationSpeed;
            /// <summary>
            /// The rotation speed.
            /// </summary>
            public float RotationSpeed
            {
                get { return rotationSpeed; }
            }

            // is this particle still alive? once TimeSinceStart becomes greater than
            // Lifetime, the particle should no longer be drawn or updated.
            /// <summary>
            /// If the particle is currently active.
            /// </summary>
            public bool Active
            {
                get { return TimeSinceStart < Lifetime; }
            }


            // initialize is called by ParticleSystem to set up the particle, and prepares
            // the particle for use.
            /// <summary>
            /// Constructs a new particle.
            /// </summary>
            /// <param name="position"></param>
            /// <param name="velocity"></param>
            /// <param name="acceleration"></param>
            /// <param name="lifetime"></param>
            /// <param name="scale"></param>
            /// <param name="rotationSpeed"></param>
            public Particle(Vector2 position, Vector2 velocity, Vector2 acceleration,
                float lifetime, float scale, float rotationSpeed)
            {
                // set the values to the requested values
                this.position = position;
                this.velocity = velocity;
                this.acceleration = acceleration;
                this.lifetime = lifetime;
                this.scale = scale;
                this.rotationSpeed = rotationSpeed;

                // reset TimeSinceStart - we have to do this because particles will be
                // reused.
                this.timeSinceStart = 0.0f;

                // set rotation to some random value between 0 and 360 degrees.
                this.rotation = Perovich.GameObjects.ComponentsExtensions.RandomBetween(0, MathHelper.TwoPi);

            }

            /// <summary>
            /// Indicates whether this instance and a specified object are equal.
            /// </summary>
            /// <param name="obj">Another object to compare to.</param>
            /// <returns>A <see cref="Boolean"/> indicating whether this instance and the specified object are equal.</returns>
            public override bool Equals(object obj)
            {
                Particle? pn = obj as Particle?;
                if (pn != null)
                {
                    Particle p = pn.Value;
                    if ((p.Acceleration == Acceleration) &&
                        (p.Lifetime == Lifetime) &&
                        (p.Position == Position) &&
                        (p.Rotation == Rotation) &&
                        (p.RotationSpeed == RotationSpeed) &&
                        (p.Scale == Scale) &&
                        (p.TimeSinceStart == TimeSinceStart) &&
                        (p.Velocity == Velocity))
                    {
                        return true;
                    }
                    else
                    {
                        return false;
                    }
                }
                return false;
            }

            /// <summary>
            /// Gets a hash code for this <see cref="Particle"/>.
            /// </summary>
            /// <returns>a hash code.</returns>
            public override int GetHashCode()
            {
                return this.Acceleration.GetHashCode() + this.Lifetime.GetHashCode() + this.Position.GetHashCode() + this.Rotation.GetHashCode() + this.RotationSpeed.GetHashCode() + this.Scale.GetHashCode() + this.TimeSinceStart.GetHashCode() + this.Velocity.GetHashCode();
            }

            /// <summary>
            /// Compares two <see cref="Particle"/> instances for equality.
            /// </summary>
            /// <param name="first">First <see cref="Particle"/></param>
            /// <param name="second">Second <see cref="Particle"/></param>
            /// <returns>A <see cref="Boolean"/> indicating whether the first and the second <see cref="Particle"/> are equal.</returns>
            public static bool operator ==(Particle first, Particle second)
            {
                return first.Equals(second);
            }

            /// <summary>
            /// Compares two <see cref="Particle"/> instances for inequality.
            /// </summary>
            /// <param name="first">First <see cref="Particle"/></param>
            /// <param name="second">Second <see cref="Particle"/></param>
            /// <returns>A <see cref="Boolean"/> indicating whether the first and the second <see cref="Particle"/> are not equal.</returns>
            public static bool operator !=(Particle first, Particle second)
            {
                return !(first == second);
            }

            /// <summary>
            /// Constructs a new particle from another particles timing values.
            /// </summary>
            /// <param name="position"></param>
            /// <param name="velocity"></param>
            /// <param name="acceleration"></param>
            /// <param name="lifetime"></param>
            /// <param name="scale"></param>
            /// <param name="rotationSpeed"></param>
            /// <param name="time"></param>
            /// <param name="rotation"></param>
            public Particle(Vector2 position, Vector2 velocity, Vector2 acceleration,
                float lifetime, float scale, float rotationSpeed, float time, float rotation)
            {
                // set the values to the requested values
                this.position = position;
                this.velocity = velocity;
                this.acceleration = acceleration;
                this.lifetime = lifetime;
                this.scale = scale;
                this.rotationSpeed = rotationSpeed;

                // reset TimeSinceStart - we have to do this because particles will be
                // reused.
                this.timeSinceStart = time;

                // set rotation to some random value between 0 and 360 degrees.
                this.rotation = rotation;
            }

            // update is called by the ParticleSystem on every frame. This is where the
            // particle's position and that kind of thing get updated.

            /// <summary>
            /// Updates the particles parameters and returns a new particle.
            /// </summary>
            /// <param name="dt"></param>
            /// <returns></returns>
            public Particle Update(float dt)
            {
                return new Particle(position + velocity * dt, velocity + acceleration * dt, acceleration, lifetime, scale, rotationSpeed, timeSinceStart + dt, rotation + rotationSpeed * dt);
            }
        }



        /// <summary>these two values control the order that particle systems are drawn in.
        /// typically, particles that use additive blending should be drawn on top of
        /// particles that use regular alpha blending. ParticleSystems should therefore
        /// set their DrawOrder to the appropriate value in InitializeConstants, though
        /// it is possible to use other values for more advanced effects.</summary>
        protected const int AlphaBlendDrawOrder = 100;
        /// <summary>these two values control the order that particle systems are drawn in.
        /// typically, particles that use additive blending should be drawn on top of
        /// particles that use regular alpha blending. ParticleSystems should therefore
        /// set their DrawOrder to the appropriate value in InitializeConstants, though
        /// it is possible to use other values for more advanced effects.</summary>
        protected const int AdditiveDrawOrder = 200;


        Random r;
        internal Texture2D texture;
        private Vector2 origin;
        private int howManyEffects;

        /// <summary>
        /// The array of particles managed by this system.
        /// </summary>
        protected Particle[] Particles;

        Queue<int> freeParticles;
        /// <summary>
        /// Gets the number of particles that are available for a new effect.
        /// </summary>
        public int FreeParticleCount
        {
            get { return freeParticles.Count; }
        }


        // This region of values control the "look" of the particle system, and should 
        // be set by deriving particle systems in the InitializeConstants method. The
        // values are then used by the virtual function InitializeParticle. Subclasses
        // can override InitializeParticle for further
        // customization.
        #region constants to be set by subclasses

        /// <summary>
        /// MinimumParticleNumber and MaximumParticleNumber control the number of particles that are
        /// added when AddParticles is called. The number of particles will be a random
        /// number between MinimumParticleNumber and MaximumParticleNumber.
        /// </summary>
        protected int MinimumParticleNumber;
        /// <summary>
        /// MinimumParticleNumber and MaximumParticleNumber control the number of particles that are
        /// added when AddParticles is called. The number of particles will be a random
        /// number between MinimumParticleNumber and MaximumParticleNumber.
        /// </summary>
        protected int MaximumParticleNumber;

        /// <summary>
        /// this controls the texture that the particle system uses. It will be used as
        /// an argument to ContentManager.Load.
        /// </summary>
        protected string textureFileName;

        /// <summary>
        /// minInitialSpeed and maxInitialSpeed are used to control the initial velocity
        /// of the particles. The particle's initial speed will be a random number 
        /// between these two. The direction is determined by the function 
        /// PickRandomDirection, which can be overriden.
        /// </summary>
        protected float minInitialSpeed;
        /// <summary>
        /// minInitialSpeed and maxInitialSpeed are used to control the initial velocity
        /// of the particles. The particle's initial speed will be a random number 
        /// between these two. The direction is determined by the function 
        /// PickRandomDirection, which can be overriden.
        /// </summary>
        protected float maxInitialSpeed;

        /// <summary>
        /// minAcceleration and maxAcceleration are used to control the acceleration of
        /// the particles. The particle's acceleration will be a random number between
        /// these two. By default, the direction of acceleration is the same as the
        /// direction of the initial velocity.
        /// </summary>
        protected float minAcceleration;
        /// <summary>
        /// minAcceleration and maxAcceleration are used to control the acceleration of
        /// the particles. The particle's acceleration will be a random number between
        /// these two. By default, the direction of acceleration is the same as the
        /// direction of the initial velocity.
        /// </summary>
        protected float maxAcceleration;

        /// <summary>
        /// minRotationSpeed and maxRotationSpeed control the particles' angular
        /// velocity: the speed at which particles will rotate. Each particle's rotation
        /// speed will be a random number between minRotationSpeed and maxRotationSpeed.
        /// Use smaller numbers to make particle systems look calm and wispy, and large 
        /// numbers for more violent effects.
        /// </summary>
        protected float minRotationSpeed;
        /// <summary>
        /// minRotationSpeed and maxRotationSpeed control the particles' angular
        /// velocity: the speed at which particles will rotate. Each particle's rotation
        /// speed will be a random number between minRotationSpeed and maxRotationSpeed.
        /// Use smaller numbers to make particle systems look calm and wispy, and large 
        /// numbers for more violent effects.
        /// </summary>
        protected float maxRotationSpeed;

        /// <summary>
        /// minLifetime and maxLifetime are used to control the lifetime. Each
        /// particle's lifetime will be a random number between these two. Lifetime
        /// is used to determine how long a particle "lasts." Also, in the base
        /// implementation of Draw, lifetime is also used to calculate alpha and scale
        /// values to avoid particles suddenly "popping" into view
        /// </summary>
        protected float minLifetime;
        /// <summary>
        /// minLifetime and maxLifetime are used to control the lifetime. Each
        /// particle's lifetime will be a random number between these two. Lifetime
        /// is used to determine how long a particle "lasts." Also, in the base
        /// implementation of Draw, lifetime is also used to calculate alpha and scale
        /// values to avoid particles suddenly "popping" into view
        /// </summary>
        protected float maxLifetime;

        /// <summary>
        /// to get some additional variance in the appearance of the particles, we give
        /// them all random scales. the scale is a value between minScale and maxScale,
        /// and is additionally affected by the particle's lifetime to avoid particles
        /// "popping" into view.
        /// </summary>
        protected float minScale;
        /// <summary>
        /// to get some additional variance in the appearance of the particles, we give
        /// them all random scales. the scale is a value between minScale and maxScale,
        /// and is additionally affected by the particle's lifetime to avoid particles
        /// "popping" into view.
        /// </summary>
        protected float maxScale;

        /// <summary>
        /// different effects can use different blend states. fire and explosions work
        /// well with additive blending, for example.
        /// </summary>
        protected BlendState blendState;

        #endregion

        private ICanHoldComponents game;
        /// <summary>
        /// Constructs a new <see cref="ParticleSystem"/>.
        /// </summary>
        /// <param name="game">The host for this particle system. The game keeps the 
        /// content manager and sprite batch for us.</param>
        /// <param name="howManyEffects">the maximum number of particle effects that
        /// are expected on screen at once.</param>
        /// <remarks>it is tempting to set the value of howManyEffects very high.
        /// However, this value should be set to the minimum possible, because
        /// it has a large impact on the amount of memory required, and slows down the
        /// Update and Draw functions.</remarks>
        protected ParticleSystem(ICanHoldComponents game, int howManyEffects)
            : base(ParticleManager.Initialize(game))
        {
            this.game = game;
            this.howManyEffects = howManyEffects;
            r = new Random();

            DrawScreen = false;

            BackgroundColor = Color.Transparent;
        }






        /// <summary>
        /// override the base class's Initialize to do some additional work; we want to
        /// call InitializeConstants to let subclasses set the constants that we'll use.
        /// 
        /// also, the particle array and freeParticles queue are set up here.
        /// </summary>
        public override void Initialize()
        {
            InitializeConstants();

            // calculate the total number of particles we will ever need, using the
            // max number of effects and the max number of particles per effect.
            // once these particles are allocated, they will be reused, so that
            // we don't put any pressure on the garbage collector.
            Particles = new Particle[howManyEffects * MaximumParticleNumber];
            freeParticles = new Queue<int>(howManyEffects * MaximumParticleNumber);
            for (int i = 0; i < Particles.Length; i++)
            {
                Particles[i] = new Particle();
                freeParticles.Enqueue(i);
            }
            base.Initialize();
        }

        /// <summary>
        /// this abstract function must be overriden by subclasses of ParticleSystem.
        /// It's here that they should set all the constants marked in the region
        /// "constants to be set by subclasses", which give each ParticleSystem its
        /// specific flavor.
        /// </summary>
        protected abstract void InitializeConstants();

        /// <summary>
        /// Override the base class LoadContent to load the texture. once it's
        /// loaded, calculate the origin.
        /// </summary>
        protected override void LoadContent()
        {
            // make sure sub classes properly set textureFileName.
            if (string.IsNullOrEmpty(textureFileName))
            {
                /*string message = "textureFileName wasn't set properly, so the " +
                    "particle system doesn't know what texture to load. Make " +
                    "sure your particle system's InitializeConstants function " +
                    "properly sets textureFileName.";
                throw new InvalidOperationException(message);*/
            }
            else
            {
                // load the texture....
                texture = game.Content.Load<Texture2D>(textureFileName);
            }

            // ... and calculate the center. this'll be used in the draw call, we
            // always want to rotate and scale around this point.
            origin.X = texture.Width / 2;
            origin.Y = texture.Height / 2;


            Width = GraphicsDevice.Viewport.Width;
            Height = GraphicsDevice.Viewport.Height;
            ParticleManager.Instance.Width = Width;
            ParticleManager.Instance.Height = Height;

            base.LoadContent();
        }

        /// <summary>
        /// AddParticles's job is to add an effect somewhere on the screen. If there 
        /// aren't enough particles in the freeParticles queue, it will use as many as 
        /// it can. This means that if there not enough particles available, calling
        /// AddParticles will have no effect.
        /// </summary>
        /// <param name="where">where the particle effect should be created</param>
        public void AddParticles(Vector2 where)
        {
            // the number of particles we want for this effect is a random number
            // somewhere between the two constants specified by the subclasses.
            int numParticles =
                r.Next(MinimumParticleNumber, MaximumParticleNumber);

            // create that many particles, if you can.
            for (int i = 0; i < numParticles && freeParticles.Count > 0; i++)
            {
                // grab a particle from the freeParticles queue, and Initialize it.
                int particleIndex = freeParticles.Dequeue();
                InitializeParticle(particleIndex, where);
            }
        }

        /// <summary>
        /// InitializeParticle randomizes some properties for a particle, then
        /// calls initialize on it. It can be overriden by subclasses if they 
        /// want to modify the way particles are created. For example, 
        /// SmokePlumeParticleSystem overrides this function make all particles
        /// accelerate to the right, simulating wind.
        /// </summary>
        /// <param name="particleIndex">the particle index to initialize</param>
        /// <param name="where">the position on the screen that the particle should be
        /// </param>
        protected virtual void InitializeParticle(int particleIndex, Vector2 where)
        {
            // first, call PickRandomDirection to figure out which way the particle
            // will be moving. velocity and acceleration's values will come from this.
            Vector2 direction = PickRandomDirection();

            // pick some random values for our particle
            float velocity =
                Perovich.GameObjects.ComponentsExtensions.RandomBetween(minInitialSpeed, maxInitialSpeed);
            float acceleration =
                Perovich.GameObjects.ComponentsExtensions.RandomBetween(minAcceleration, maxAcceleration);
            float lifetime =
                Perovich.GameObjects.ComponentsExtensions.RandomBetween(minLifetime, maxLifetime);
            float scale =
                Perovich.GameObjects.ComponentsExtensions.RandomBetween(minScale, maxScale);
            float rotationSpeed =
                Perovich.GameObjects.ComponentsExtensions.RandomBetween(minRotationSpeed, maxRotationSpeed);

            // then initialize it with those random values. initialize will save those,
            // and make sure it is marked as active.
            Particles[particleIndex] = new Particle(
                where, velocity * direction, acceleration * direction,
                lifetime, scale, rotationSpeed);
        }

        /// <summary>
        /// PickRandomDirection is used by InitializeParticles to decide which direction
        /// particles will move. The default implementation is a random vector in a
        /// circular pattern.
        /// </summary>
        protected virtual Vector2 PickRandomDirection()
        {
            float angle = Perovich.GameObjects.ComponentsExtensions.RandomBetween(0, MathHelper.TwoPi);
            return new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
        }

        /// <summary>
        /// overriden from DrawableComponent, Update will update all of the active
        /// particles.
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            if (gameTime == null)
                throw new ArgumentNullException("gameTime");
            // calculate dt, the change in the since the last frame. the particle
            // updates will use this value.
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            // go through all of the particles...
            for (int i = 0; i < Particles.Length; i++ )
            {
                if (Particles[i].Active)
                {
                    // ... and if they're active, update them.
                    Particles[i] = Particles[i].Update(dt);
                    // if that update finishes them, put them onto the free particles
                    // queue.
                    if (!Particles[i].Active)
                    {
                        freeParticles.Enqueue(i);
                    }
                }
            }

            base.Update(gameTime);
        }



        /// <summary>
        /// overriden from Screen, Draw will render all of the active particles.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {

            for (int i = 0; i < Particles.Length; i++)
            {
                // skip inactive particles
                if (!Particles[i].Active)
                    continue;

                // normalized lifetime is a value from 0 to 1 and represents how far
                // a particle is through its life. 0 means it just started, .5 is half
                // way through, and 1.0 means it's just about to be finished.
                // this value will be used to calculate alpha and scale, to avoid 
                // having particles suddenly appear or disappear.
                float normalizedLifetime = Particles[i].TimeSinceStart / Particles[i].Lifetime;

                // we want particles to fade in and fade out, so we'll calculate alpha
                // to be (normalizedLifetime) * (1-normalizedLifetime). this way, when
                // normalizedLifetime is 0 or 1, alpha is 0. the maximum value is at
                // normalizedLifetime = .5, and is
                // (normalizedLifetime) * (1-normalizedLifetime)
                // (.5)                 * (1-.5)
                // .25
                // since we want the maximum alpha to be 1, not .25, we'll scale the 
                // entire equation by 4.
                float alpha = 4 * normalizedLifetime * (1 - normalizedLifetime);
                Color color = Color.White * alpha;

                // make particles grow as they age. they'll start at 75% of their size,
                // and increase to 100% once they're finished.
                float scale = Particles[i].Scale * (.75f + .25f * normalizedLifetime);

                SBatches.CurrentBatch.Draw(texture, Particles[i].Position, null, color,
                    Particles[i].Rotation, origin, scale, SpriteEffects.None, Depth);
            }
            base.Draw(gameTime);
        }

    }
}
